The Unofficial Newsletter of Delphi Users - by Robert Vivrette


More With Microsoft Agent: Let There Be Tools!

by François Gaillard

I found these articles (Microsoft Agent article , More on Microsoft Agent) on Microsoft Agent very interesting, and had some fun playing with the example. Then I had some questions...

  1. can I load more than one character,
  2. what if I want to play with other characters,
  3. text is written very slowly into the balloon, can we speed things up,
  4. how can we know which animations are playable by a character,
  5. and what about an animation browser to make it easy.
Some Answers...

More than one character:

A single Agent can load many (I did not see the limit, it may be somewhere in the docs) different characters with two limitations:

A default character can be interesting if you want to respect the choice of your user, or do not want to care about which has been installed.
The default character is loaded with a code like:
Agent.Characters.Load('Default', UnAssigned);
Where ‘Default’ is (as usual) the name you want to give to that loaded character. You then can call it by:
Agent.Characters.Character('Default'...
Of course, you can call it «genie» if you want, even if it is Merlin.

Lot of characters:

To load many characters, you just call load more than once:

Agent.Characters.Load('Genie', 'Genie.acs');
Agent.Characters.Load('Merlin', 'Merlin.acs');
Agent.Characters.Load('Peedy', 'Peedy.acs');
Agent.Characters.Load('Robby', 'Robby.acs');
You will get an EOleException ‘File not found’ if  the corresponding *.ACS file is not installed. Remember that:
Agent.Characters.Load('Genie', 'Merlin.acs');
Agent.Characters.Load('Merlin', 'Genie.acs');
is correct, though a bit dirty.

Speeding text writing into the balloon
The speed at which an Agent writes into the balloon depends of the text-to-speech settings, and cannot be changed by program! It is a per User setting, and developpers are just allowed to read the speed value.
So, if you are like a lot of developpers and prefer to set things yourself without interference of the users, there is a trick, done by the MaxSpeedSpeach procedure.
We’ll simulate the user opening the text-to-speech dialog:

anAgent.PropertySheet.Visible:=true;
anAgent.PropertySheet.Page  := 'Output';
Then find the window handle of the speed slider, and the appropriate message:
sendMessage(w,TBM_SETPOS,1,100);
Then find the window handle of the OK button and send a click:
sendMessage(w,BM_CLICK, 0, 0);
The trick is of course to «find the window handle». Winsight gave us that the parent form is a «#32770» class window, and the slider is a «msctls_trackbar32» class window. The button is of course a «Button» class window, and we need also it’s title «OK»to find it. We use an EnumProc callback function where we pass a reference to an array of 3 pointers, corresponding respectively to the classname, title (in) and the handle (out) of the searched window. I do not know if these class names are worldwhile the same… so better check it!

Dynamically listing the available Animations

If you look at the Character interface you’ll find a AnimationNames property which has an enumeration. Using an IEnumVariant interface to loop with it’s next method gave the list of the available animations.
This is the purpose of the GetAnimationList procedure. You give it a Character argument, and a TStrings where you’ll get back the list of all Animations names usable as argument to the play method of that Character.

Design

Now, let’s build a comfortable program. I have freely modified the AgentDemo program (revised by Nikolai Botev) to add a sort of «Animation browser».

First, I wanted to have the main characters available: Genie, Merlin, Peedy and Robby. So I modified the FormCreate to load all these 4 characters. I dropped a RadioGroup to select which of them is to be under control. I draw a TlistBox (sorted) to handle the Animations List. With a onclick event to launch the clicked animation. And a button to retrieve the animations list of the selected character. I did not want to build it automatically when the character selection changes because I wanted to compare quickly the animations amongst the characters. And so you can test what happens when you play a wrong animation: for instance Merlin has less idle_x ones than Genie. I should have put the MaxSpeedSpeach routine at the FormCreate level, but I wanted to see that «property sheet» blinking at each time I rebuild the list. Sort of a little revenge…

Code

( the complete project is zipped in AgentDemo2.zip)

procedure TfrmMain.FormShow(Sender: TObject);
begin
//     Agent.Characters.Load('Default', UnAssigned);
  try
    Agent.Characters.Load('Genie', 'Genie.acs');
  except
  end;
  try
    Agent.Characters.Load('Merlin', 'Merlin.acs');
  except
  end;
  try
    Agent.Characters.Load('Peedy', 'Peedy.acs');
  except
  end;
  try
    Agent.Characters.Load('Robby', 'Robby.acs');
  except
  end;
end;
 

procedure TfrmMain.btnGetAnimClick(Sender: TObject);
begin
  MaxSpeedSpeach(Agent);
//  GetAnimationList(Agent.Characters.Character('default'),ListBox1.items);
  with rdgCharacters do
    if ItemIndex>-1 then
      GetAnimationList(Agent.Characters.Character(Items[ItemIndex]),ListBox1.items);
end;

procedure TfrmMain.ListBox1Click(Sender: TObject);
begin
  with Sender as TListBox do
    if ItemIndex>-1 then
      with Agent.Characters.Character(rdgCharacters.Items[rdgCharacters.ItemIndex]) do begin
        show(True);              // ensure it is visible
        stopAll('');             // stop previous animations
        play(items[itemindex]);  // play the choosen one
      end;
end;
 

{ Get a list of all animations (for the play method) existing for a character}
procedure GetAnimationList(IAChar:IAgentCtlCharacterEx;Ts:TStrings);
{ IAChar : the character like  Agent.Characters.Character('Genie')
  Ts : the TStrings to be filled with the list of his available animations}
const
  { avoid a use clause on OLE2 }
  IID_IEnumVariant: TGUID = (
    D1:$00020404;D2:$0000;D3:$0000;D4:($C0,$00,$00,$00,$00,$00,$00,$46));
var
  pEnum:IEnumVARIANT ;
  vAnimName:VARIANT ;
  dwRetrieved:DWORD ;
  hRes: HResult;
begin
  hRes:=IAChar.AnimationNames.enum.QueryInterface(IID_IEnumVARIANT, pEnum);
  if S_Ok=hRes then begin
    if Ts.count<>0 then begin
      IAChar.show(True);  //0=false, plays animation if any; 1=true, quicker show without anim
      IAChar.Speak('I will clear the List!','');
//      IAChar.Think('I will clear the List!');
      ts.Clear;
    end;
    while (TRUE) do begin
      hRes:=pEnum.Next(1, vAnimName, @dwRetrieved);
      if S_OK<>hRes then
        break;
      // vAnimName is the animation Name
      ts.add(vAnimName);
      //VariantClear(&vAnimName);
    end;
    //pEnum.free();
  end;
end;

{ set the speaking speed at it's highest }
procedure MaxSpeedSpeach(anAgent:TAgent);
type
  arp= array[0..2] of Pointer;
const
  s1:PChar='msctls_trackbar32';
  s2:PChar='Button';
  s3:PChar='OK';
var
  h,w:THandle;
  p: arp;

  { the callback function for EnumChildWindows }
  function EnumProc(W:THandle; LP:LParam):Wordbool; stdcall;
  { we pass the address of an array of 3 pointers as LParam
    INPUTs : pointers to classname and title of the desired window
    OUTPUT : Pointer to the found window handle;
  type
    arp= array[0..2] of Pointer;                                      }
  var
    s:PChar;
  begin
    Result:=true;
    s:=strAlloc(255);
    try
      if GetClassName(w,s,254)>0 then begin
        if strIComp(s,PChar(arp(Pointer(LP)^)[0]^))=0 then begin
        {$IFDEF DEBUG}
          ShowMessage('ok    pchar1');
        {$ENDIF}
          if arp(Pointer(LP)^)[1]<>nil then begin
            // look for caption
            if GetWindowText(w,s,100)=0 then begin
              exit;
            end else begin
              if strIComp(s,PChar(arp(Pointer(LP)^)[1]^))<>0 then begin
                 exit;
              end;
            end;
          end;
          // we have found the desired window
          arp(Pointer(LP)^)[2]:=@w;
          Result:=false; // to stop enumeration
        end;
      {$IFDEF DEBUG}
      end else begin
        ShowMessage(IntToStr(GetLastError));
        ShowMessage(s);
      {$ENDIF}
      end;
    finally
      strDispose(s);
    end;
  end; { EnumProc }

begin  { MaxSpeedSpeach }
  // Open the agent property dialog
  anAgent.PropertySheet.Visible:=true;
  anAgent.PropertySheet.Page  := 'Output';

  // find the Parent window containing the speed slider
  h:=findWindow('#32770',nil);
{$IFDEF DEBUG}
  ShowMessage(IntToStr(h));
{$ENDIF}

  // find the slider : 'msctls_trackbar32','Slider1'
  p[0]:=@s1;
  p[1]:=nil;
  EnumChildWindows(h,@EnumProc,Longint(@p));
  w:=PLongInt(p[2])^;
  // found the slider, send it to the maximum speed
  sendMessage(w,TBM_SETPOS,1,100);
{$IFDEF DEBUG}
  ShowMessage(IntToStr(w));
{$ENDIF}

  // find the OK Button 'Button','OK'
  p[0]:=@s2;
  p[1]:=@s3;
  EnumChildWindows(h,@EnumProc,Longint(@p));
  w:=PLongInt(p[2])^;
  // found the OK Button; send a click to Close dialog
  sendMessage(w,BM_CLICK, 0, 0);
{$IFDEF DEBUG}
  ShowMessage(IntToStr(w));
{$ENDIF}
end;  { MaxSpeedSpeach }